AI챗봇_03_RAG 구축과 운영 최적화
RAG(Retrieval-Augmented Generation) 구축
단순히 OpenAI와 대화만 하는 챗봇이 아닌, 내가 쓴 글들을 기억하고 그것을 바탕으로 대답해주는 것이 RAG(Retrieval-Augmented Generation)이다. 이를 위해서는 내 글들을 AI가 이해할 수 있는 숫자의 배열(Vector)로 바꿔서 저장할 저장소가 필요하다.
여러 선택지가 있었지만 Redis Stack을 선택했다. 가장 대중적인 인메모리 DB이면서 벡터 검색 기능을 기본으로 지원하기 때문이다. 무엇보다 메모리 사용량을 엄격하게 제한할 수 있어, 1GB 램을 가진 소형 서버에서도 충분히 운용 가능하다.
Docker Compose 인프라 구성
앱 서버와 Redis가 한 몸처럼 움직여야 하기에 docker-compose.yml을 작성했다. 가장 신경 쓴 부분은 메모리 제한이다.
services:
redis:
image: redis/redis-stack-server:latest
deploy:
resources:
limits:
memory: 128M # 벡터 데이터는 생각보다 작다 (OpenAI vector 1개 \u2248 6KB)
chatbot-server:
build: .
deploy:
resources:
limits:
memory: 300M # 앱 구동을 위한 최소한의 마지노선
Redis에 할당한 128MB는 작아 보이지만, 텍스트 청크(Chunk) 약 10,000개 이상을 저장할 수 있는 충분한 공간이다.
데이터 동기화 파이프라인
RAG 파이프라인의 핵심은 정적 파일과 동적 DB 간의 동기화다. 내 블로그는 git push로 배포되는 정적(Static) 파일의 집합이고 벡터 DB는 동적(Dynamic)이다. 파일이 수정되거나 삭제되었을 때 DB의 데이터도 동일하게 반영되어야 환각(Hallucination) 현상을 막을 수 있다.
해시 기반 배치 동기화
자원이 부족한 환경에서 실시간 감시(Watcher) 대신 파일의 해시(Hash) 값을 이용한 배치 동기화 전략을 택했다.
- Source of Truth: 빌드 파이프라인이 생성한
analysis-cache.json에는 모든 문단의 해시값이 계산되어 있다. - Diff Logic: 배포 직후
/api/ingest/sync가 호출되면 현재 DB 상태와analysis-cache.json을 비교한다.- New: 새로운 해시라면
VectorStore.add()를 수행한다. - Modified: 해시가 변경되었다면 기존 벡터 삭제 후 재삽입(Upsert)한다.
- Unchanged: 해시가 같다면 작업을 생략하여 자원을 절약한다.
- New: 새로운 해시라면
이 전략을 통해 불필요한 임베딩(Embedding) 비용을 절감하고 항상 최신의 지식 상태를 유지할 수 있다.
보안 및 비용 최적화
프론트엔드 연동 전 오픈된 AI 서비스의 비용 공격(Wallet Exhaustion Attack)에 대비해야 한다.
방어적 프롬프트 엔지니어링(Defensive Engineering)
비용 이전에 탈옥(Jailbreak) 시도나 엉뚱한 사용을 막기 위해 애플리케이션 레벨에서 방어막을 쳤다.
- Prompt Injection 감지:
ignore instruction,act as등 해킹 시도 문구가 포함되면 아예 LLM에 보내지 않고 차단한다. - 입력 검증 (Input Validation): 질문 길이를 50자로 제한하여 토큰 과소비를 원천 봉쇄한다.
Hard Limit
소프트웨어 외부의 안전장치로 OpenAI 대시보드에서 한도도 설정했다. 어떤 공격이 들어와도 지출은 설정한 한도 내에서 멈추게 되어 비용 측면에서의 안전을 보장한다.
스마트 라우팅과 비용 최적화
메타 질문 감지
모든 질문에 LLM을 호출하면 비용이 발생한다. 하지만 "파이썬은 얼마나 알지?"처럼 특정 문서가 아닌 전체 지식 현황을 묻는 메타 질문은 기존에 가진 데이터만으로도 충분히 응답할 수 있다.
// 메타 질문 패턴 정의
private val metaQuestionPatterns = listOf(
Regex("(.+)(은|는)\\s*(얼마나|뭘)\\s*(알|배웠)"),
Regex("(뭘|무엇을)\\s*(알|배웠)")
)
의도 분류 기반 라우팅
질문이 들어오면 다음 순서로 처리한다.
- 프롬프트 인젝션 감지: 악성 패턴이면 즉시 차단
- Canned Response: 하드코딩된 Q&A와 일치하면 즉시 반환
- 메타 질문 감지: Knowledge Graph 캐시로 응답 (LLM 호출 X)
- 일반 질문: 기존 RAG 파이프라인 (Vector Search → LLM)
다음과 같이 정리해볼 수 있다.
- 의도 분류(Intent Classification): NLP의 핵심 개념 이해
- 비용 최적화 설계: 클라우드 자원을 효율적으로 사용하는 엔지니어링
- 시스템 통합: 프론트엔드의 Knowledge Graph 데이터를 백엔드에서 활용
홈 화면에 채팅창 심기
백엔드(Backend)가 준비되었으니 이제 프론트엔드(Frontend) 차례다.
블로그 홈 화면에 챗봇을 넣기로 했다.
프로젝트 카드 패널을 좌측으로 이동시키고, 해당 위치에 챗봇 인터페이스를 넣었다.
다른 페이지에서도 플로팅 버튼 등으로 작동하게 해야하나 고민 중인데, 일단은 홈에서만 작동하는 걸로.
디자인은 블로그 톤 맞춰서 간단하게 정리했다.
LLM과 Redis, 그리고 보안 설정, 프론트엔드 연결까지 모든 준비가 끝났다.
배포만 잘 되면 좋겠다....